﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Microsoft.VisualBasic;
using System.Runtime.CompilerServices;
using System.IO;

namespace AquesTalk
{

    public class ImeLanguage
    {
        //by emanual.IME

        private bool FInitialized = false;

        private const int S_OK = 0;
        private const int CLSCTX_LOCAL_SERVER = 4;
        private const int CLSCTX_INPROC_SERVER = 1;
        private const int CLSCTX_INPROC_HANDLER = 2;
        private const int CLSCTX_SERVER = CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER;
        private const int FELANG_REQ_REV = 0x00030000;
        private const int FELANG_CMODE_PINYIN = 0x00000100;
        private const int FELANG_CMODE_NOINVISIBLECHAR = 0x40000000;

        [DllImport("ole32.dll")]
        private static extern int CLSIDFromString([MarshalAs(UnmanagedType.LPWStr)] string lpsz, out Guid pclsid);

        [DllImport("ole32.dll")]
        private static extern int CoCreateInstance([MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
            IntPtr pUnkOuter, uint dwClsContext, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr rpv);

        [DllImport("ole32.dll")]
        private static extern int CoInitialize(IntPtr pvReserved);

        [DllImport("ole32.dll")]
        private static extern int CoUninitialize();

        //-------------------------------------------------------------------------------------
        // コンストラクタ
        public ImeLanguage()
        {
            int res = CoInitialize(IntPtr.Zero);

            if (res == S_OK)
                FInitialized = true;
        }

        public void Dispose()
        {
            if (FInitialized)
            {
                CoUninitialize();
                FInitialized = false;
            }
        }

        // デストラクタ
        ~ImeLanguage()
        {
            if (FInitialized)
            {
                CoUninitialize();
                //                ifeLang.Close();
            }
        }

        IntPtr ppv;
        IFELanguage ifeLang;
        public static object YomiLocker = new object();
        public Regex acceptChars = new Regex(@"[^ -~、。A-Za-z0-9Ａ-Ｚａ-ｚ０－９\p{IsHiragana}\p{IsKatakana}\p{IsCJKUnifiedIdeographs}]", RegexOptions.Compiled);
        public string GetYomi(string str)
        {
            str = acceptChars.Replace(str, "");
            lock (YomiLocker)
            {
                if (str == null || str == String.Empty)
                    return str;
                int res;
                Guid pclsid;
                // 文字列の CLSID から CLSID へのポインタを取得する
                res = CLSIDFromString("MSIME.Japan", out pclsid);

                if (res != S_OK)
                    throw new Exception("CLSID pointer translation failed.");

                Guid riid = new Guid("019F7152-E6DB-11D0-83C3-00C04FDDB82E");
                res = CoCreateInstance(pclsid, IntPtr.Zero, CLSCTX_SERVER, riid, out ppv);

                if (res != S_OK)
                    throw new Exception("Instance create failed. :" + res.ToString("X"));

                ifeLang = Marshal.GetTypedObjectForIUnknown(ppv, typeof(IFELanguage)) as IFELanguage;
                res = ifeLang.Open();

                if (res != S_OK)
                    throw new Exception("Language interface open failed.");
                string yomi = String.Empty;
                //int res;

                IntPtr result;
                res = ifeLang.GetJMorphResult(FELANG_REQ_REV, FELANG_CMODE_PINYIN | FELANG_CMODE_NOINVISIBLECHAR,
                        str.Length, str, IntPtr.Zero, out result);

                if (res != S_OK)
                    throw new Exception("Convert error.");

                try
                {
                    yomi = Marshal.PtrToStringUni(Marshal.ReadIntPtr(result, 4), Marshal.ReadInt16(result, 8));
                    return yomi;
                }
                catch (AccessViolationException)
                {
                    return String.Empty;
                }
                finally
                {
                    ifeLang.Close();
                    ifeLang = null;
                }
            }
        }

        public string[] GetYomies(string[] strs, Action<int> callback)
        {
            List<string> converted = new List<string>();
            int res;
            Guid pclsid;
            // 文字列の CLSID から CLSID へのポインタを取得する
            res = CLSIDFromString("MSIME.Japan", out pclsid);

            if (res != S_OK)
                throw new Exception("CLSID pointer translation failed.");

            Guid riid = new Guid("019F7152-E6DB-11D0-83C3-00C04FDDB82E");
            res = CoCreateInstance(pclsid, IntPtr.Zero, CLSCTX_SERVER, riid, out ppv);

            if (res != S_OK)
                throw new Exception("Instance create failed. :" + res.ToString("X"));

            ifeLang = Marshal.GetTypedObjectForIUnknown(ppv, typeof(IFELanguage)) as IFELanguage;
            res = ifeLang.Open();

            if (res != S_OK)
                throw new Exception("Language interface open failed.");
            for (int i = 0; i < strs.Length; i++)
            {
                if (strs[i] == null || strs[i] == String.Empty)
                {
                    converted.Add("");
                    continue;
                }
                //int res;
                IntPtr result;
                res = ifeLang.GetJMorphResult(FELANG_REQ_REV, FELANG_CMODE_PINYIN | FELANG_CMODE_NOINVISIBLECHAR,
                        strs[i].Length, strs[i], IntPtr.Zero, out result);
                if (res != S_OK)
                    throw new Exception("Convert error.");
                converted.Add(Marshal.PtrToStringUni(Marshal.ReadIntPtr(result, 4), Marshal.ReadInt16(result, 8)));
                callback.Invoke(i);
            }
            ifeLang.Close();

            return converted.ToArray();
        }
    } // end of ImeLanguage class

    //**************************************************************************************
    // IFELanguage Interface（メソッドの実装はランタイムの中にあるので、実装は不要）
    //**************************************************************************************
    [ComImport, Guid("019F7152-E6DB-11D0-83C3-00C04FDDB82E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IFELanguage
    {
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        int Open();
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        int Close();
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        int GetJMorphResult(uint dwRequest, uint dwCMode, int cwchInput,
            [MarshalAs(UnmanagedType.LPWStr)] string pwchInput, IntPtr pfCInfo, out IntPtr ppResult);
    } // end of IFELanguage Interface

    public class AquesTalk
    {

        #region Singleton
        private static readonly AquesTalk instance = new AquesTalk();
        public static AquesTalk Instance { get { return instance; } }
        private AquesTalk()
        {
            LoadReplaceTables();
        }

        ~AquesTalk()
        {
            ime.Dispose();
        }
        #endregion

        public void InitImeSvc()
        {
            ime = new ImeLanguage();
        }

        ImeLanguage ime;

        const string SimpleReplaceFile = "smplrep.txt";
        const string ComplexReplaceFile = "cplxrep.txt";
        public void LoadReplaceTables()
        {
            simpleReplaceTable.Clear();
            complexReplaceTable.Clear();
            using (StreamReader sr = new StreamReader(SimpleReplaceFile))
            {
                while (!sr.EndOfStream)
                {
                    var str = sr.ReadLine();
                    if (str.StartsWith("#"))
                        continue;
                    if (!str.Contains('\t'))
                        continue;
                    var items = str.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
                    if (items.Length < 2)
                        continue;
                    AddSimpleReplace(items[0], items[1]);
                }
            }
            using (StreamReader sr = new StreamReader(ComplexReplaceFile))
            {
                while (!sr.EndOfStream)
                {
                    var str = sr.ReadLine();
                    if (str.StartsWith("#"))
                        continue;
                    if (!str.Contains('\t'))
                        continue;
                    var items = str.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
                    if (items.Length < 2)
                        continue;
                    if (items.Length == 3 && items[0] == "!")
                        AddComplexReplace(new Regex(items[1], RegexOptions.Singleline), items[2]);
                    else
                        AddComplexReplace(new Regex(items[0], RegexOptions.IgnoreCase | RegexOptions.Singleline), items[1]);
                }
            }
        }

        IntPtr parentWndHandle;
        public void SetParentWindowForm(System.Windows.Forms.Form form)
        {
            parentWndHandle = form.Handle;
        }

        public const int CallbackMsg = 0xA910; //AquTo
        private IntPtr WndHookProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg == CallbackMsg)
            {
                uint val = Convert.ToUInt32(lParam.ToInt32());
                if (waitingEngines.ContainsKey(val))
                {
                    AquesTalkDa_Release(waitingEngines[val]);
                    waitingEngines.Remove(val);
                }
            }
            return IntPtr.Zero;
        }

        public void PlayEndSignalReceived(uint val)
        {
            if (waitingEngines.ContainsKey(val))
            {
                AquesTalkDa_Release(waitingEngines[val]);
                waitingEngines.Remove(val);
            }
        }

        volatile uint ___currentCounter = 0;
        uint GetCurrentCounter()
        {
            return ___currentCounter++;
        }
        Dictionary<uint, IntPtr> waitingEngines = new Dictionary<uint, IntPtr>();

        [DllImport("AquesTalkDa.dll")]
        static extern int AquesTalkDa_PlaySync(string koe, int iSpeed);
        [DllImport("AquesTalkDa.dll")]
        static extern int AquesTalkDa_Play(IntPtr hMe, string koe, int iSpeed, IntPtr Wnd, uint msg, uint dwUser);
        [DllImport("AquesTalkDa.dll")]
        static extern IntPtr AquesTalkDa_Create();
        [DllImport("AquesTalkDa.dll")]
        static extern void AquesTalkDa_Release(IntPtr hMe);

        private Dictionary<string, string> simpleReplaceTable = new Dictionary<string, string>();
        private Dictionary<Regex, string> complexReplaceTable = new Dictionary<Regex, string>();

        public void AddSimpleReplace(string orig, string dest)
        {
            if (simpleReplaceTable.ContainsKey(orig))
                simpleReplaceTable[orig] = dest;
            else
                simpleReplaceTable.Add(orig, dest);
        }

        public void AddComplexReplace(Regex rgx, string repl)
        {
            if (complexReplaceTable.ContainsKey(rgx))
                complexReplaceTable[rgx] = repl;
            else
                complexReplaceTable.Add(rgx, repl);
        }

        public Regex acceptableTokens = new Regex(@"[^A-Za-z<>=0-9あ-んア-ンー'、\.。/ ]", RegexOptions.Compiled);
        public Exception prevExcp;
        /// <summary>
        /// 直接発声
        /// </summary>
        public int Speak(string kana, int speedPercent)
        {
            kana = acceptableTokens.Replace(kana, "");
            var at = AquesTalkDa_Create();
            uint cc = GetCurrentCounter();
            waitingEngines.Add(cc, at);
            try
            {
                return AquesTalkDa_PlaySync(
                    kana,
                    speedPercent);
 /*
                return AquesTalkDa_Play(
                    at,
                    kana,
                    speedPercent,
                    parentWndHandle,
                    CallbackMsg, cc);
  */
            }
            catch (Exception e)
            {
                prevExcp = e;
                return -1;
            }
        }

        public string GetErrorText(int errorcode)
        {
            switch (errorcode)
            {
                case -1:
                    return "[.NET エラー]" + prevExcp.ToString();
                case 101:
                case 203:
                    return "メモリが不足しています";
                case 102:
                case 105:
                    return "読めない文字が含まれています";
                case 103:
                    return "[内部エラー]韻律データが不正です";
                case 104:
                    return "[内部エラー]未定義の区切りコードが検出されました";
                case 106:
                    return "音声記号のタグが不正です";
                case 107:
                    return "タグが長すぎるか、終了タグがありません";
                case 108:
                    return "タグ内の値が不正です";
                case 109:
                case 110:
                    return "[サウンドドライバエラー]再生に失敗しました";
                case 111:
                    return "発声すべきデータがありません";
                case 200:
                    return "音声データが長すぎます";
                case 201:
                    return "読み記号が多すぎます";
                case 202:
                case 204:
                    return "[内部エラー]音声データが長すぎます";
                default:
                    return "未定義のエラーです";
            }
        }


        /// <summary>
        /// 内部フィルタ噛み噛み
        /// </summary>
        /// <param name="source"></param>
        public int Talk(string source, int speedPercent)
        {
            string src = ime.GetYomi(source);
            src = Strings.StrConv(src, VbStrConv.Wide, 0);
            src = Strings.StrConv(src, VbStrConv.Katakana, 0);
            src = Regex.Replace(src, "[０-９]", (m) =>
            {
                char ch = (char)('0' + (m.Value[0] - '０'));
                return ch.ToString();
            });
            src = src.Replace('．', '.');

            foreach (var rgx in complexReplaceTable.Keys)
                src = rgx.Replace(src, complexReplaceTable[rgx]);
            foreach (var str in simpleReplaceTable.Keys)
                src = src.Replace(str, simpleReplaceTable[str]);
            return Speak(src, speedPercent);
        }

        /// <summary>
        /// コンバート用
        /// </summary>
        public string[] PreConvTalk(string[] sources, Action<int> callback)
        {
            string[] srcs = ime.GetYomies(sources, callback);
            for (int i = 0; i < srcs.Length; i++)
            {
                srcs[i] = Strings.StrConv(srcs[i], VbStrConv.Wide, 0);
                srcs[i] = Strings.StrConv(srcs[i], VbStrConv.Katakana, 0);
                srcs[i] = Regex.Replace(srcs[i], "[０-９]", (m) =>
                {
                    char ch = (char)('0' + (m.Value[0] - '０'));
                    return ch.ToString();
                });
                srcs[i] = srcs[i].Replace('．', '.');

                foreach (var rgx in complexReplaceTable.Keys)
                    srcs[i] = rgx.Replace(srcs[i], complexReplaceTable[rgx]);
                foreach (var str in simpleReplaceTable.Keys)
                    srcs[i] = srcs[i].Replace(str, simpleReplaceTable[str]);
            }
            return srcs;
        }
    }
}